AOP Custom Contract: ListNotEmptyAttribute
So I have a List
using System; using System.Collections.Generic; namespace CustomContractsExample { public class People { private readonly List<Person> _People; public People(List<Person> people) { if (people == null) throw new ArgumentNullException("people", "The list cannot be null."); if (people.Count == 0) throw new ArgumentException("people", "The list cannot be empty."); _People = people; } public List<Person> List { get { return _People; } } } }
Note: My use case is not actually a People object with a List
I decided to handle this precondition checking not in the methods, but in an Aspect. Particularly, by using a PostSharp LocationContractAttribute. I recently wrote a post about this here:
AOP Contracts with PostSharp
So we need to create a new custom contract as I didn’t find one written by PostSharp. At first, I wondered why not. Why not create a quick generic attribute like this:
using System.Collections.Generic; using PostSharp.Aspects; using PostSharp.Patterns.Contracts; using PostSharp.Reflection; namespace CustomContractsExample { public class ListNotEmptyAttribute<T> : LocationContractAttribute, ILocationValidationAspect<List<T>> { new public const string ErrorMessage = "The List<T> must not be empty."; protected override string GetErrorMessage() { return "The List<T> must not be empty: {2}"; } public System.Exception ValidateValue(List<T> value, string locationName, LocationKind locationKind) { if (value == null) return CreateArgumentNullException(value, locationName, locationKind); if (value.Count == 0) return CreateArgumentException(value, locationName, locationKind); return null; } } }
Well, the reason is because C# doesn’t support generic attributes. I get this error at compile time:
A generic type cannot derive from ‘LocationContractAttribute’ because it is an attribute class
This is a tragedy. What makes it more of a tragedy is that I could do this if I wrote directly in IL. It is simply a compiler limitation for C#. Arrrgggss!!!! Good thing MSBuild is going open source at https://github.com/Microsoft/msbuild. Hopefully, the DotNet team, or some interested party such as PostSharp, or maybe me, contributes a few changes to MSBuild and removes this limitation.
As for now, List
using System.Collections; using PostSharp.Aspects; using PostSharp.Patterns.Contracts; using PostSharp.Reflection; namespace CustomContractsExample { public class ListNotEmptyAttribute : LocationContractAttribute, ILocationValidationAspect<IList> { new public const string ErrorMessage = "The List must not be empty."; protected override string GetErrorMessage() { return "The list must not be empty: {2}"; } public System.Exception ValidateValue(IList value, string locationName, LocationKind locationKind) { if (value == null) return CreateArgumentNullException(value, locationName, locationKind); if (value.Count == 0) return CreateArgumentException(value, locationName, locationKind); return null; } } }
Now here is the new People class. See how it is much cleaner.
using System.Collections.Generic; namespace CustomContractsExample { public class People { private readonly List<Person> _People; public People([ListNotEmpty]List<Person> people) { _People = people; } public List<Person> List { get { return _People; } } } }
The constructor is much cleaner and easier to read.
Also, my unit tests pass.
using System; using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; using CustomContractsExample; namespace CustomContractsExampleTests { [TestClass] public class PeopleTests { // Arrange private const string Firstname = "Jared"; private const string LastName = "Barneck"; [TestMethod] public void TestNewPersonWorks() { var person = new Person(Firstname, LastName); var list = new List<Person> { person }; var people = new People(list); Assert.IsNotNull(people); Assert.IsFalse(people.List.Count == 0); } [TestMethod] [ExpectedException(typeof(ArgumentNullException))] public void TestNewPersonThrowsExceptionIfFirstNameNull() { new People(null); } [TestMethod] [ExpectedException(typeof(ArgumentException))] public void TestNewPersonThrowsExceptionIfLastNameNull() { new People(new List<Person>()); } } }
Maybe PostSharp can pick this up my ListNotEmptyAttribute and add it to their next released version.